package com.thiswhyme.common.utils.codec;

import com.thiswhyme.common.expection.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * @description: RSA加解密工具类
 * @author: thiswhyme
 * @date: 2021/4/13
 */
public class RSAUtils {

	private static final Logger log = LoggerFactory.getLogger(RSAUtils.class);

	/**
	 * RSA最大加密明文大小
	 */
	private static final int MAX_ENCRYPT_BLOCK = 117;

	/**
	 * RSA最大解密密文大小
	 */
	private static final int MAX_DECRYPT_BLOCK = 128;

	/**
	 * 签名算法-MD5
	 */
	public static final String SIGN_TYPE_MD5 = "MD5withRSA";

	/**
	 * 签名算法-SHA1
	 */
	public static final String SIGN_TYPE_SHA1 = "SHA1withRSA";

	/**
	 * 签名算法-SHA256
	 */
	public static final String SIGN_TYPE_SHA256 = "SHA256WithRSA";

	/**
	 * 公钥加密
	 * @param data 待加密数据
	 * @param publicKeyByte 公钥字二进制
	 * @return 密文二进制
	 */
	public static byte[] encryptByPublicKey(String data, byte[] publicKeyByte) {
		byte[] result;
		try {
			PublicKey publicKey = getPublicKey(publicKeyByte);
			result = encrypt(data, publicKey);
		} catch (Exception e) {
			log.error("RSA公钥加密异常", e);
			throw new BusinessException("RSA公钥加密异常");
		}
		return result;
	}

	/**
	 * 私钥加密
	 * @param data 待加密数据
	 * @param privateKeyByte 私钥二进制
	 * @return 密文二进制
	 */
	public static byte[] encryptByPrivateKey(String data, byte[] privateKeyByte) {
		byte[] result;
		try {
			PrivateKey privateKey = getPrivateKey(privateKeyByte);
			result = encrypt(data, privateKey);
		} catch (Exception e) {
			log.error("RSA私钥加密异常", e);
			throw new BusinessException("RSA私钥加密异常");
		}
		return result;
	}

	/**
	 * 公钥解密
	 * @param data 待解密数据二进制
	 * @param publicKeyByte 公钥字二进制
	 * @return 原始数据二进制
	 */
	public static byte[] decryptByPublicKey(byte[] data, byte[] publicKeyByte) {
		byte[] result;
		try {
			PublicKey publicKey = getPublicKey(publicKeyByte);
			result = decrypt(data, publicKey);
		} catch (Exception e) {
			log.error("RSA公钥解密异常", e);
			throw new BusinessException("RSA公钥解密异常");
		}
		return result;

	}

	/**
	 * 私钥解密
	 * @param data 待解密数据二进制
	 * @param privateKeyByte 私钥二进制
	 * @return 原始数据二进制
	 */
	public static byte[] decryptByPrivateKey(byte[] data, byte[] privateKeyByte) {
		byte[] result;
		try {
			PrivateKey privateKey = getPrivateKey(privateKeyByte);
			result = decrypt(data, privateKey);
		} catch (Exception e) {
			log.error("RSA私钥解密异常", e);
			throw new BusinessException("RSA私钥解密异常");
		}
		return result;
	}

	/**
	 * 获取私钥
	 * @param publicKey 私钥二进制
	 * @return
	 * @throws Exception
	 */
	private static PrivateKey getPrivateKey(byte[] publicKey) throws Exception {
		KeyFactory keyFactory = KeyFactory.getInstance(EncryptAlgorithm.RSA.name());
		PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(publicKey);
		return keyFactory.generatePrivate(keySpec);
	}

	/**
	 * 获取公钥
	 * @param privateKey 公钥二进制
	 * @return
	 * @throws Exception
	 */
	private static PublicKey getPublicKey(byte[] privateKey) throws Exception {
		KeyFactory keyFactory = KeyFactory.getInstance(EncryptAlgorithm.RSA.name());
		X509EncodedKeySpec keySpec = new X509EncodedKeySpec(privateKey);
		return keyFactory.generatePublic(keySpec);
	}

	/**
	 * RSA加密
	 * @param data 待加密数据
	 * @param key 密钥
	 * @return
	 * @throws Exception
	 */
	private static byte[] encrypt(String data, Key key) throws Exception {
		Cipher cipher = Cipher.getInstance(EncryptAlgorithm.RSA.name());
		cipher.init(Cipher.ENCRYPT_MODE, key);
		int inputLen = data.getBytes().length;
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		int offset = 0;
		byte[] cache;
		int i = 0;

		while (inputLen - offset > 0) {
			if (inputLen - offset > MAX_ENCRYPT_BLOCK) {
				cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK);
			} else {
				cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset);
			}
			out.write(cache, 0, cache.length);
			i++;
			offset = i * MAX_ENCRYPT_BLOCK;
		}
		byte[] encryptedData = out.toByteArray();
		out.close();

		return encryptedData;
	}

	/**
	 * RSA解密
	 * @param dataBytes 待解密数据二进制
	 * @param key 密钥
	 * @return
	 * @throws Exception
	 */
	private static byte[] decrypt(byte[] dataBytes, Key key) throws Exception {
		Cipher cipher = Cipher.getInstance(EncryptAlgorithm.RSA.name());
		cipher.init(Cipher.DECRYPT_MODE, key);
		int inputLen = dataBytes.length;
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		int offset = 0;
		byte[] cache;
		int i = 0;

		while (inputLen - offset > 0) {
			if (inputLen - offset > MAX_DECRYPT_BLOCK) {
				cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
			} else {
				cache = cipher.doFinal(dataBytes, offset, inputLen - offset);
			}
			out.write(cache, 0, cache.length);
			i++;
			offset = i * MAX_DECRYPT_BLOCK;
		}
		byte[] decryptedData = out.toByteArray();
		out.close();
		return decryptedData;
	}

	/**
	 * 签名
	 * @param data 待签名数据
	 * @param privateKeyByte 私钥二进制
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 签名
	 */
	public static byte[] sign(String data, byte[] privateKeyByte, String signType) {
		byte[] result;
		try {
			byte[] keyBytes = getPrivateKey(privateKeyByte).getEncoded();
			PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
			KeyFactory keyFactory = KeyFactory.getInstance(EncryptAlgorithm.RSA.name());
			PrivateKey key = keyFactory.generatePrivate(keySpec);
			Signature signature = Signature.getInstance(signType);
			signature.initSign(key);
			signature.update(data.getBytes());
			result = signature.sign();
		} catch (Exception e) {
			log.error("{}签名异常", signType, e);
			throw new BusinessException("签名异常");
		}
		return result;
	}

	/**
	 * 验签
	 * @param data 原始字符串
	 * @param publicKeyByte 公钥二进制
	 * @param sign 签名二进制
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 是否验签通过
	 */
	public static boolean verify(String data, byte[] publicKeyByte, byte[] sign, String signType) {
		boolean result;
		try {
			byte[] keyBytes = getPublicKey(publicKeyByte).getEncoded();
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
			KeyFactory keyFactory = KeyFactory.getInstance(EncryptAlgorithm.RSA.name());
			PublicKey key = keyFactory.generatePublic(keySpec);
			Signature signature = Signature.getInstance(signType);
			signature.initVerify(key);
			signature.update(data.getBytes());
			result = signature.verify(sign);
		} catch (Exception e) {
			log.error("{}验签异常", signType, e);
			throw new BusinessException("验签异常");
		}
		return result;
	}

	/**
	 * 签名
	 * @param data 待签名数据
	 * @param privateKeyBase64Str 私钥字符串（base64）
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 签名（base64）
	 */
	public static String signToBase64String(String data, String privateKeyBase64Str, String signType) {
		byte[] privateKeyByte = CodecUtils.base64String2Bytes(privateKeyBase64Str);
		byte[] result =  sign(data, privateKeyByte, signType);
		return  CodecUtils.bytes2Base64String(result);
	}

	/**
	 * 验签
	 * @param data 原始字符串
	 * @param publicKeyBase64Str 公钥字符串（base64）
	 * @param signBase64Str 签名字符串（base64）
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 是否验签通过
	 */
	public static boolean verifyFromBase64String(String data, String publicKeyBase64Str, String signBase64Str, String signType) {
		return verify(data, CodecUtils.base64String2Bytes(publicKeyBase64Str), CodecUtils.base64String2Bytes(signBase64Str), signType);
	}

	/**
	 * 签名
	 * @param data 待签名数据
	 * @param privateKeyHexStr 私钥字符串（16进制字符串）
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 签名（16进制字符串）
	 */
	public static String signToHexString(String data, String privateKeyHexStr, String signType) {
		byte[] result =  sign(data, CodecUtils.hexStringToByte(privateKeyHexStr), signType);
		return  CodecUtils.byte2HexString(result);
	}

	/**
	 * 验签
	 * @param data 原始字符串
	 * @param publicKeyHexStr 公钥字符串（16进制字符串）
	 * @param signHexStr 签名字符串（16进制字符串）
	 * @param signType 签名算法 （this.SIGN_TYPE_MD5、this.SIGN_TYPE_SHA1）
	 * @return 是否验签通过
	 */
	public static boolean verifyFromHexString(String data, String publicKeyHexStr, String signHexStr, String signType) {
		return verify(data,  CodecUtils.hexStringToByte(publicKeyHexStr), CodecUtils.hexStringToByte(signHexStr), signType);
	}
}